PTR Design Kit
A plan to ingest the PTR Design Kit into the Priority Hub app and standardize it as the de-facto system across every PTR surface — with one versioned package as the single source of truth, and the brand/deck assets kept fully usable outside the app. Phase 1 is closed: v0.2.0 tagged July 22, and distribution moved the same day to jsDelivr off the public repo — no registry, no auth; the files serve, and Phase 2 (the Priority Hub app swap) is next.
The Priority Hub app and this design system already speak the same language. The app is built on Tailwind v4 + the DaisyUI plugin (input.css → output.css), and its live theme is declared in og_dashboard.css as [data-theme="light"] / [data-theme="dark"] blocks. Those blocks set the exact same CSS variables our ptr-theme.css defines — --color-primary, --color-base-100, --radius-box, and so on. Today they hold an Apple-style blue palette (#0071e3); our theme holds PTR Green (#086033).
What this means
Consuming the design system is not a rebuild — it is a theme swap. Because the app's component CSS already reads DaisyUI tokens (not hardcoded hex), replacing the variable values re-skins the entire app to PTR. Every button, badge, card, sidebar and leaderboard medal inherits the brand the moment the tokens change. This is the whole reason DaisyUI was chosen, and it's why "source of truth" is achievable cheaply.
One directional flow, no parallel copies. Figma is where design decisions are made; a single published package is what everything installs. Nothing hand-maintains a second copy of the tokens.
PTR.tokens.json via export, and are released by bumping the package version. No consumer ever hand-edits token values.AUDIT_PLAN.md) tracks the remaining backlog.@plugin "daisyui/theme" block and the runtime [data-theme] CSS are both generated from PTR.tokens.json — never edited by hand in two places — closing audit item #1 (two parallel systems that drift the moment a token changes) permanently. And because component CSS references tokens and never hardcodes values, a single package bump re-skins every surface at once — which is exactly what makes it a real source of truth rather than a style guide people ignore.Per your call, we ship a single scoped package and let each consumer link only the layer it needs. Served from the public design-kit repo via jsDelivr — no registry, no auth anywhere; every consumer pins a v* tag in the URL. (Revised July 22: GitHub Packages dropped before any consumer installed.)
// package.json "exports" — consumers cherry-pick their layer { "./theme.css": // DaisyUI "PTR" theme — the tokens the app needs "./theme.plugin.css": // @plugin block for Tailwind+DaisyUI builds "./theme-dark.css": // NEW (July 16) — branded dark theme, selector-gated (D5) "./brand.css": // extensions: brand palette, case-status, type scale "./marketing.css" // + "./pages.css" — marketing/web layer, canonical July 17 "./product.css": // shared product layer — shipped in v0.2 (July 17) "./tokens.json": // + "./tokens.dark.json" — the source-of-truth token sets "./assets/*" // logo lockups, textures, fonts config }
The DaisyUI "PTR" theme. This is all the Priority Hub app strictly needs — it re-skins every DaisyUI component.
The 8 case-status colors, extended brand palette, type scale, spacing and motion tokens DaisyUI has no slot for — plus (July 16) layout tokens (--site-max/--gutter/--header-h), surfaces (--surface-1/2) and the Inter eyebrow pair (.ptr-eyebrow quiet/--loud).
The shared product vocabulary — status pills (--case-{group}-bg/-fg), empty states, filter bars, toasts. Deferred from v0.1, then shipped in v0.2 (July 17) under budget rule 2: vocabulary used by 2+ apps lives here, never copied per app. App-bespoke chrome stays app-side.
The Services-site web vocabulary — header/nav, hero, section bands, service cards, accordions, lead form, footer, dark theme. Same tokens, no DaisyUI dependency; the style contract for the Django/Wagtail rebuild. Packaged July 17: canonical copies now ship as package/marketing.css + pages.css; ui_kits/marketing/ re-exports them.
Logos, grain texture, fonts and the brand palette — everything the marketing/deck side pulls without touching the app.
You chose to replace the app's light / dark theme values with PTR — which means zero changes to templates or JavaScript. The Alpine binding :data-theme="darkMode ? 'dark' : 'light'" stays exactly as it is; we only change what those two theme names resolve to.
No install — three tag-pinned jsDelivr <link>s in the base template, after the compiled output.css: theme.css, brand.css, theme-dark.css.
input.css alone@plugin "daisyui" { themes: light, dark } stays — the built-ins remain registered as a fallback. Loading after output.css, theme.css overrides light to PTR and theme-dark.css re-flips dark to the branded dark. Theme names unchanged, so the Alpine binding is untouched.
Remove the [data-theme="light"] / [data-theme="dark"] variable blocks from og_dashboard.css. They become redundant — the package now owns those values. All the app-specific component rules below them stay untouched.
brand.css ships in the step-1 link stack, so case-status colors and the type scale are available app-wide. Swap the Google Fonts <link> for the package's font config to guarantee the exact Inter weights.
npm run build:css. The dashboard renders in PTR Green with no template edits. Spot-check contrast on badges and the leaderboard (audit item #8).
<link rel="stylesheet" href="{% static 'css/output.css' %}">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/PriorityTaxRelief-Development/design-kit@v0.2.0/package/theme.css"> // PTR tokens → override "light"
<link rel="stylesheet" href="…/design-kit@v0.2.0/package/brand.css"> // extension tokens + .ptr-* classes
<link rel="stylesheet" href="…/design-kit@v0.2.0/package/theme-dark.css"> // branded dark → re-flips "dark"
priority_hub/static/css/og_dashboard.css — remove these, keep everything else
[data-theme="light"] { --color-primary: #0071e3; --color-base-100: #fff; … } [data-theme="dark"] { --color-primary: #2997ff; … } /* ✓ keep: .og-sidebar, .og-lb-card, .dispo-chip, all component rules — they read var(--color-*) and now inherit PTR automatically */
Same package, same entry point everywhere: tag-pinned jsDelivr links — identical URLs whether or not the platform can run a build (jsDelivr auto-serves minified .min.css variants). Repos that want the theme registered at build time vendor theme.plugin.css from the tagged URL. The brand/deck side stays independent by design: it consumes only brand.css (palette, type, logos, textures) and the standalone slides/ templates — never the DaisyUI component layer — so a product CSS change can never break a slide, and a deck asset can never leak into the product bundle. One source of truth, two clean audiences.
lighttheme + brand + marketing.min.css)tokens.json is the two-way bridge (Tokens Studio / Figma variables)brand.css + assets/slides/ stay standaloneAudit of the Services project (July 15, 2026) against the system: token coverage is solid — its ptr-tokens.css matches the package exactly, and both site stylesheets use zero hardcoded brand hex. But the site made five design decisions the system doesn't cover. They were ingested verbatim into ui_kits/marketing/ and logged here — decided & executed July 16, 2026. This log now also tracks the audit's open items (#). Resolved rows are grayed; open rows need your call.
| # | Discrepancy | Site says | System says | Decision · status |
|---|---|---|---|---|
| D1type | Marketing type scale | 17px body; responsive clamp() headings at weight 800–900 | Fixed-px 1920-grid deck scale (--fs-*), weight 400 | Resolved — system style. Body → --fs-body/--lh-body; display headings weight 400 with token-capped clamps (--fs-h1/h2/h3/display). Component titles kept at 800 → outlier O1; mid sizes → O2; px line-heights → O3. Follow-up July 16: the original weight-800 display headings return as an opt-in variation — body[data-headings="heavy"] in the marketing layer; weight 400 stays the standard. |
| D2type | Eyebrow style | .eyebrow: Inter 700 · 13px · orange | .ptr-eyebrow: Outfit · 12px · graphite | Resolved — converged on Inter. Quiet (.ptr-eyebrow / .eyebrow--quiet: graphite 600·12) + loud (.ptr-eyebrow--loud / .eyebrow: orange 700·13), both in brand.css. Outfit stays for measure/spec labels only. |
| D3layout | Layout tokens defined locally | --site-max: 1240px, --gutter: 40px, --header-h: 84px | No layout tokens exist | Resolved — promoted. All three now in package/brand.css; the marketing-local :root block is gone. Figma-side variables pending the next tokens.json sync (O5). |
| D4motion | Motion values | Own --ease: cubic-bezier(.22,.61,.36,1); literal 80/120/140/180ms | --ease-out/--ease-in-out, --duration-press/fast/base/slow (80/120/180/320) — added after the site shipped | Resolved — system tokens. All 77 transitions on --duration-* + --ease-out: 80→press, 120/140→fast, 160–240→base, 300→slow. The 600ms entrance reveal stays literal → O4. |
| D5theme | Dark mode | Full hand-rolled html[data-theme="dark"] token flip: --surface-1/2, brightened green, light logo chips — WCAG AA, shipped | No dark theme (audit #12: neutral placeholder) | Resolved — promoted. New package/theme-dark.css ([data-theme="dark"] / [data-theme="ptr-dark"]) holds the token flip incl. --surface-1/2; marketing cards/inputs are now surface-token driven (~15 dark overrides deleted, undefined --text-1/--border-1 refs killed). tokens.json fold-in pending Figma dark mode (O5). |
| O1type | Component-title weight | Card/job/post titles: 19–24px at weight 800 | Display type now weight 400; system h4 weight is 600 | Resolved — kept 800, made a style. Sanctioned as .ptr-title (--fs-title 21 / --lh-title 1.25) in package/brand.css; canonical card/job titles now reference the token. |
| O2type | Mid-size text gap | Web copy needs 17–22px roles (leads 20–22, intros 19, prose 18) | Scale jumps 16 → 28 — no slot between body and lead | Resolved — tokens added. --fs-lead-web 20 · --fs-intro 19 · --fs-body-lg 18 (+ --fs-title 21, O1), each with a unitless --lh-*; marketing layer consumes them (hero leads, intros, prose, bands). |
| O3type | px line-height tokens | Marketing uses unitless equivalents under responsive clamp() | --lh-h2/h3/lead/display are fixed px off the 1920 deck grid | Resolved — unitless. Re-expressed as ratios (98/96 → 1.0208, 46/42 → 1.0952, 34/28 → 1.2143) — deck output unchanged; marketing .section h2 now uses --lh-h2 directly. |
| O4motion | 600ms entrance reveal | Scroll-entrance reveal animates at 600ms (kept literal) | Duration tokens cap at --duration-slow 320ms | Resolved — existing token. The reveal now runs on --duration-slow (320ms) — no new token; the last literal duration in the marketing layer is gone. |
| O5tokens | Figma sync debt | Layout tokens (D3) + dark theme (D5) hand-authored in the package | tokens.json is a single-mode export with no layout group — build.mjs doesn't own them | Resolved — folded in. tokens.json gains color.brand (12 Pantone-mapped) + color.surface + layout groups; new tokens.dark.json carries the dark mode (only the variables that flip). build.mjs now generates theme-dark.css and the brand/layout/surface blocks in brand.css — ready to import into Figma. |
| #8a11y | Accessibility / contrast | PTR Orange used decoratively across surfaces | No text-vs-fill rule; orange must never carry body text on white | Resolved — rule set July 16. Orange is a selective accent: never small text (loud eyebrows, card/post links, hover links → green; form-error micro-text → the error pair); large heavy display headlines may be orange (hero accents, stat suffixes stay); as a fill only on the key CTA, always with dark text (buttons already comply; ::selection fixed). Documented in package/brand.css. Badge + leaderboard contrast spot-check remains in the Phase-2 app swap. Documented July 30 — audit #4 closed: every sanctioned pairing measured (README Accessibility & contrast + two live-computing preview cards). Two exported feedback pairs came back under AA for normal text → new item 17. |
| #17a11y · tokens | Feedback pairs under AA | success-content on success = 3.58:1; info-content on info = 4.45:1 — straight from the Figma export | AA needs 4.5:1 for normal text | Open — needs a Figma edit. Darken the two -content values in the "PTR" theme, re-export tokens.json, npm run build. Not hand-fixable: token values only enter through the build. Documented as fills/large-text-only until then; status text uses the --case-* pill pairs, which all clear AA. |
| #2/#5bundle | Bundle hygiene | StatusBadge cross-file dependency; demo fixtures shipped in the public bundle | Publish-blocker for the first release | Resolved — July 15. StatusBadge extracted with a declared dependency; fixtures module-local; CaseList takes a cases prop. |
| #6assets | Heavy grain asset | Brand layer shipped a ~8 MB grain texture | Package must stay lean | Resolved — July 15. Compressed 116 KB grain.jpg ships; full-res master stays repo-only. |
| #12theme | Branded dark theme | Neutral dark placeholder in the app | Audit called for a branded dark pass (was Phase 3) | Resolved — July 16. Landed early as package/theme-dark.css via D5; tokens.json fold-in tracked as O5. Neutral dark dropped from the app plan (July 16, later) — dark resolves to the branded theme from day one. |
Also pendingproduct.css — component layer, deferred by decision (July 15); revived for v0.2 as the shared product layer (July 17, custom-CSS budget rule 2): vocabulary used by 2+ apps (status pills, empty states, filter bars, toasts) ships here, never copied per app.
| Phase | What ships | Outcome |
|---|---|---|
| PackagePhase 1 · foundation · shipped July 22 | Restructured into the publishable package: token build from tokens.json, exports map, CI release workflow. v0.2.0 tagged in the design-kit repo — made public July 22, served tag-pinned by jsDelivr (distribution revised from GitHub Packages the same day). | A versioned single source of truth exists and serves. |
| App swapPhase 2 · Priority Hub · next | Re-theme light to PTR, delete inline overrides, load brand layer, verify contrast. Close audit items #8 (contrast) and #2/#5 (bundle hygiene). | The flagship app is fully PTR-branded, zero template rewrites. |
| Fan-outPhase 3 · everywhere | Landing pages onto the package; HubSpot + Ads onto the same jsDelivr links (auto-minified); wire the Figma ↔ tokens.json sync; branded dark theme (#12 — landed early, July 16, via package/theme-dark.css). | De-facto system across every PTR surface. |
Running record of the build-out, updated as steps land. Last update July 22, 2026. Phase 1 closed with the July 22 release.
package/ with package.json: exports map, files list, semver scripts (the publishConfig came out on July 22 with the jsDelivr switch). Docs in package/README.md.build.mjs generates theme.css, theme.plugin.css and the @generated block in brand.css from tokens.json; --check runs in CI on every v* tag, so a stale file fails the release. The anti-drift rule is now enforced, not aspirational..ptr-* classes, spacing, shadows; new motion tokens (--duration-*, --ease-*) close audit #13. Tokens + classes only — safe to load app-wide.package/assets/logo/; grain texture compressed 7.9 MB → 116 KB grain.jpg (audit #6). Full-res master stays repo-only.ptr-theme.css re-exports package/theme.css; colors_and_type.css imports package/brand.css and keeps only preview element defaults. No second copy of any token remains.StatusBadge extracted to its own component with a declared .d.ts dependency; demo fixtures now module-local, off window and the namespace; CaseList takes an optional cases prop. Publish-blocker cleared.uploads/ removed: the tokens JSON now lives canonically at package/tokens.json, and Group 130.svg duplicated ptr-flag-black.svg.assets/logo/ deleted (11 SVGs were byte-identical to package/assets/logo/ with no sync check); every reference (previews, slides, product kit, templates, docs) repointed at the package copy. One copy, zero drift.index.html renders the README content + all 30 preview cards + 8 slides as a navigable brand-guideline page; .nojekyll added so _ds_* files serve correctly on GitHub Pages.PriorityTaxRelief-Development/ptr_logo (closes the "Figma-lifted logos" caveat): 4 vector masters ship in the package at assets/logo-kit/svg/; high-res PNGs (Horz 4800, Stacked 4800, Icon 2400), favicon set, BIMI and social covers stay repo-only in assets/logo-kit/ to keep npm lean. New Downloads section on index.html.site.css + pages.css pulled verbatim into ui_kits/marketing/ (dev-only preview switcher stripped) with README, specimen card, and the D1–D5 discrepancy log above. Behaviors stay site-side by design.--fs-body, weight-400 headings with token-capped clamps); eyebrows converged on Inter as quiet + loud variants in brand.css; layout tokens (--site-max/--gutter/--header-h) promoted; all 77 transitions on the --duration/--ease-out tokens; dark mode promoted to package/theme-dark.css with new --surface-1/2 tokens (closes audit #12). Five outliers O1–O5 logged above.var(--ease)/literal-duration transitions left in the marketing layer; every referenced token (--fs/--lh/--ls/--surface/--duration/--ease) resolves in brand.css; undefined --text-1/--border-1 refs eliminated; theme-dark.css wired into the package exports map, files list, and the repo preview chain (colors_and_type.css); eyebrow specimen card updated. Design-system compile clean..ptr-title (weight-800 component titles, --fs-title 21) + web-copy mid tokens --fs-lead-web/--fs-intro/--fs-body-lg consumed across the marketing layer; display line-height tokens re-expressed unitless (deck output unchanged); entrance reveal on --duration-slow — zero literal durations remain; tokens.json gains brand/surface/layout groups with a new tokens.dark.json dark mode, and build.mjs now generates theme-dark.css — the JSON pair is ready for the Figma-side import.brand.css): loud eyebrows + card/post links + hover links → green (bright variant in dark mode), form-error micro-text → the DaisyUI error pair. App plan now points the dark theme name at theme-dark.css — the neutral dark placeholder is dropped.ui_kits/product/reference.html + reference.css deliver the componentized Inbox + Case Detail specimen (.tp-* contract, both status taxonomies as 8 tint/ink groups, Django partial map + HTMX seams matched to their conventions). The custom-CSS budget (DaisyUI first · 2+ apps → package · bespoke reads tokens, never hex) is codified in the Product Surface Plan and scopes product.css for v0.2.marketing.css + pages.css moved into package/ (vocabulary only; token layers load first), wired into the exports map + files; ui_kits/marketing/ now re-exports the package copies — same no-second-copy pattern as ptr-theme.css.package/product.css carries status pills (.tp-pill--{group}), empty state, filter bar and toast, tokens only; the 8 --case-{group}-bg/-fg tint/ink pairs promoted into brand.css (hand-authored block, badge-tint rule, Figma re-export pending); wired into the exports map + files, version bumped to 0.2.0.PriorityTaxRelief-Development/design-kit; CI release workflow added (.github/workflows/publish.yml: fires on v* tags, staleness check, tag-vs-version guard, publishes with the built-in GITHUB_TOKEN — no PAT). Package renamed @prioritytaxrelief-development/design-kit (scope must match the org owner — the original @prioritytaxrelief scope 403'd). v0.2.0 is live on GitHub Packages.design-kit repo is public; consumers link tag-pinned cdn.jsdelivr.net/gh/…/package/* URLs — no .npmrc, no token. publishConfig removed and the package marked "private": true (guards against accidental registry publishes); publish.yml reduced to a tag check (staleness + tag-vs-version — the tag is the release); theme.plugin.css becomes vendor-only (a build-time @plugin block can't load from a CDN) — the Phase-2 app swap consumes the runtime files via <link> instead. Handoff, package README and this plan repointed..tp-* collapsed onto DaisyUI per the specimen's §7 map: buttons/fields/checkboxes are DaisyUI classes (previewed via ui_kits/product/daisy.css, a preview-only shim), 2px field/button borders, 4/4/8 radii, primary focus, PTR type scale (12/14/16 + tabular numerals; 10px overline kept as a local --fs-micro alias — candidate token), --space-*/--shadow-*/duration tokens throughout. The interactive kit adopted the surviving .tp-* names and app.js color maps now emit token/group references, not hex — the working-25 grouping stays one re-mappable table (WORKING_GROUP, matches the specimen §1) pending E-001.7.WORKING_TO_FINAL — one re-mappable 25→17 table, raw value in the tooltip. Remaining: business sign-off on the mapping.ui_kits/product/forms.html (§8) carries per-form field tables (types, required, conditional-on, error copy verbatim from app.js), copy-paste modal markup on DaisyUI modal + native <dialog>, and the HTMX contract (open as fragment → #tp-modal + showModal(), delayed inline validation, submit → close + toast + owning-region swap; bookkeeping points recompute server-side — BK_PTS weights backend-configured). The §7 modal collapse applied: daisy.css gained the modal/modal-box/modal-backdrop shim and the interactive kit's three modals now run on <dialog> — app.js business logic untouched, wiring only. Suggested partials: _tracking_form / _handpull_form / _bk_form / _form_field / _bk_points_bar.product.css shipped in v0.2, pages.css + tokens.dark.json listed), the stale publishConfig/prepublishOnly and "v0.1.0 is publish-ready" lines, card counts (28 preview / 40 registered), and worklog chronology. Duplication removed: design_handoff_phase2_app_swap/reference/ held second copies of theme.css, brand.css, theme-dark.css, both token JSONs and the package README — deleted in favor of tag-pinned jsDelivr URLs, since a stale token copy inside the handoff is exactly the drift this plan's anti-drift guarantee promises can't happen; and ui_kits/product/base.css now holds the import stack + :root alias block that product.css and reference.css each carried a byte-identical copy of. AUDIT_PLAN.md gains a Remaining backlog section (items 4, 10, 14, 15, 16) so open work lives in exactly one place.preview/contrast-ledger.js, so they can't go stale; summary in brand.css). Findings: text stays on ink/graphite/base-400/green, orange fills always carry dark text, status text uses the --case-* pairs (all eight clear AA), and on dark --ptr-green-bright replaces --color-primary (2.38:1). Two exported feedback pairs miss AA for normal text → item 17, a Figma-side token edit. Changelog: package/CHANGELOG.md reconstructs 0.1.0 / 0.2.0 from this worklog and ships in the package, so a consumer pinning a tag can see what a bump brings; writing the entry is now part of the release recipe.Four buckets. Only package/ ships to consumers; everything else either powers this workspace or documents the work. Audited July 15, 2026 — nothing unaccounted for remains at root.
| Bucket | Files | Rule |
|---|---|---|
| Workspace machineryClaude environment | _ds_bundle.js, _ds_manifest.json, _adherence.oxlintrc.json, thumbnail.html, styles.css (compiler entry), doc-page.js (renders this doc), templates/*/support.js · ds-base.js · deck-stage.js | Auto-generated or runtime scaffolding — never hand-edit, never publish. |
| Design systemconsumed here | README.md, SKILL.md, ptr-theme.css + colors_and_type.css (wire the package in), preview/ (30 cards; 42 registered cards in total once slides + UI kits are counted), slides/, templates/, ui_kits/product/ + ui_kits/marketing/ (the ingested Services-site web vocabulary), assets/ (working masters: full-res grain, team portrait — logos live only in the package; assets/logo-kit/ holds the official high-res PNG/favicon/BIMI/cover exports from ptr_logo for the Downloads page) | The system's docs, specimens and components. Reference — not shipped. |
| The packageserved via jsDelivr | package/ only: tokens.json + tokens.dark.json (truth) → build.mjs → theme.css / theme.plugin.css / theme-dark.css / brand.css, CHANGELOG.md, hand-authored marketing.css + pages.css (canonical, July 17) + product.css (shared product layer, v0.2), assets/ (logos + 116 KB grain) and package.json / README.md | The only publishable surface. Token edits enter via tokens.json alone. |
| Working docsproject management | Consumption Plan.html (this doc), Product Surface Plan.html, AUDIT_PLAN.md, design_handoff_phase2_app_swap/ (the engineering handoff — links the package, never copies it) | Living records — updated as phases land. Rule: a doc states a fact once and links for the rest; no doc restates a token value or a file's contents. |